﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using Microsoft.SharePoint;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Globalization;
using System.Reflection;

namespace ListItemEditor
{
    public class GridViewDataSourceSiteData : IGridViewDataSource
    {
        #region constants

        protected const int NUMBER_BATCH_ITEMS_UPDATE = 1000;
        protected const int NUMBER_BATCH_ITEMS_RESYNC = 200;

        #endregion

        #region inner types

        protected class ListContext
        {
            private Dictionary<string, Type> _fieldValueTypeMap = new Dictionary<string, Type>();

            public ListContext(SPList list)
            {
                foreach (SPField field in list.Fields) _fieldValueTypeMap[field.InternalName] = field.FieldValueType;
            }

            public Type GetFieldValueType(string fieldName)
            {
                if (this._fieldValueTypeMap.ContainsKey(fieldName)) return this._fieldValueTypeMap[fieldName];
                return null;
            }
        }
        
        #endregion

        #region variables

        protected static readonly HashSet<string> SpecialFields = new HashSet<string>() { "FileLeafRef", "owsnewfileref", "owshiddenmodstatus", "owsitemlevel", "owshiddenversion" };
        protected static PropertyInfo _eventFiringDisabled;

        #endregion

        #region properties

        protected static PropertyInfo EventFiringDisabled 
        {
            get
            {
                if (_eventFiringDisabled == null)
                {
                    _eventFiringDisabled = Assembly.GetAssembly(typeof(SPWeb)).GetType("Microsoft.SharePoint.SPEventManager").GetProperty("EventFiringDisabled", BindingFlags.Static | BindingFlags.NonPublic);
                }
                return _eventFiringDisabled;
            }
        }

        #endregion

        #region IGridViewDataSource Members

        public GridViewDataSourceSettings Settings { get; set; }

        public virtual DataTable GetData()
        {
            DataTable table = null;
            OpenUseList(delegate(SPWeb web, SPList list)
            {
                SPView view = null;
                if (!string.IsNullOrEmpty(this.Settings.ViewName)) view = list.Views[this.Settings.ViewName];
                else if (list.DefaultView != null) { view = list.DefaultView; this.Settings.ViewName = view.Title; }

                if (view == null)
                {
                    if (this.Settings.ViewFields == null || this.Settings.ViewFields.Count == 0) this.Settings.ViewFields = new List<string>() { "ID"};
                }
                else
                {
                    if (this.Settings.ViewFields == null || this.Settings.ViewFields.Count == 0) this.Settings.ViewFields = this.GetViewFieldsFromView(view);
                    if (this.Settings.Query == null) this.Settings.Query = view.Query;
                }
                this.Settings.ViewFields = FixViewFieldSchema(list, this.Settings.ViewFields);
                this.Settings.RootFolder = this.GetEffectiveRootFolder(this.Settings);
                table = this.InternalGetDataTable(web, list);
            });
            table.AcceptChanges();
            return table;
        }

        public virtual void ApplyDataChanges(DataTable table)
        {
            DataRow[] changedRows = table.Select(null, null, DataViewRowState.ModifiedCurrent | DataViewRowState.Added | DataViewRowState.Deleted);
            if (changedRows.Length == 0) return;

            UpdateItems(table, changedRows, false);
        }

        public virtual void CheckOutInFiles(List<DataRow> selectedRows, CheckOutAction checkOutAction)
        {
            if (selectedRows == null || selectedRows.Count == 0) return;

            OpenUseList(delegate(SPWeb web, SPList list)
            {
                List<DataRow> succeededRows = new List<DataRow>();
                foreach (DataRow row in selectedRows)
                {
                    try
                    {
                        string fileUrl = Utility.ParseLookupValue(row["FileRef"].ToString());
                        if (!fileUrl.StartsWith("/")) fileUrl = "/" + fileUrl;
                        SPFile file = web.GetFile(fileUrl);

                        switch (checkOutAction)
                        {
                            case CheckOutAction.CheckOut: file.CheckOut(); break;
                            case CheckOutAction.UndoCheckOut: file.UndoCheckOut(); break;
                            case CheckOutAction.CheckInMinor: file.CheckIn(string.Empty, SPCheckinType.MinorCheckIn); break;
                            case CheckOutAction.CheckInMajor: file.CheckIn(string.Empty, SPCheckinType.MajorCheckIn); break;
                            case CheckOutAction.CheckInOverwrite: file.CheckIn(string.Empty, SPCheckinType.OverwriteCheckIn); break;
                        }
                        row.ClearErrors();
                        succeededRows.Add(row);
                    }
                    catch (Exception ex)
                    {
                        row.RowError = ex.Message;
                    }
                }

                this.ApplyResyncChanges(web, list, succeededRows);
            }
            );
        }

        public void Moderate(List<DataRow> selectedRows, ModerationStatus moderationStatus)
        {
            if (selectedRows == null || selectedRows.Count == 0) return;

            foreach (DataRow row in selectedRows) row["_ModerationStatus"] = (int)moderationStatus;
            this.UpdateItems(selectedRows[0].Table, selectedRows.ToArray(), true);
        }

        #endregion

        #region virtual methods

        protected virtual string GetEffectiveRootFolder(GridViewDataSourceSettings settings)
        {
            // here no support for root folders but inheritors will have
            return null;
        }

        protected virtual DataTable InternalGetDataTable(SPWeb web, SPList list)
        {
            SPSiteDataQuery query = new SPSiteDataQuery();
            if (!string.IsNullOrEmpty(this.Settings.Query)) query.Query = this.Settings.Query;
            if (this.Settings.RowLimit > 0) query.RowLimit = this.Settings.RowLimit;

            List<string> effectiveViewFields = this.GetEffectiveViewFields(list is SPDocumentLibrary);
            if (effectiveViewFields != null && effectiveViewFields.Count > 0) query.ViewFields = FormatViewFields(effectiveViewFields);

            query.Lists = string.Format("<Lists><List ID=\"{0}\" /></Lists>", list.ID.ToString("B"));

            DataTable table = web.GetSiteData(query);
            if (table.Columns.Contains("WebId")) table.Columns.Remove("WebId");
            if (table.Columns.Contains("ListId")) table.Columns.Remove("ListId");
            return table;
        }

        protected virtual bool IsFieldTypeNotAllowed(SPField field)
        {
            // skip these types of fields
            // also multiple lookups - SPSiteDataQuery doesn't return these
            return (field is SPFieldCalculated || (field is SPFieldLookup && ((SPFieldLookup)field).AllowMultipleValues));
        }

        #endregion

        #region private helpers

        protected void UpdateItems(DataTable table, DataRow[] selectedRows, bool moderate)
        {
            OpenUseList(delegate(SPWeb web, SPList list)
            {
                try
                {
                    if (this.Settings.SuppressEvents) DisableEventFiring();
                    Dictionary<string, DataRow> rowMap = new Dictionary<string, DataRow>();

                    int nextMethodID = 0;
                    List<string> results = new List<string>();
                    foreach (DataRow[] partSelectedRows in Utility.SplitInChunks<DataRow>(selectedRows, NUMBER_BATCH_ITEMS_UPDATE))
                    {
                        string batch = this.GenerateCAML(list, table, partSelectedRows, rowMap, moderate, ref nextMethodID);
                        string result = web.ProcessBatchData(batch);
                        results.Add(result);
                    }

                    this.ResyncAfterUpdate(web, list, this.MergeResultXMLs(results.ToArray()), rowMap);
                }
                finally
                {
                    if (this.Settings.SuppressEvents) EnableEventFiring();
                }
            });
        }

        protected string MergeResultXMLs(string[] results)
        {
            if (results.Length == 0) return string.Empty;
            if (results.Length == 1) return results[0];

            XElement el = XElement.Parse(results[0]);
            el.Add(results.Skip(1).SelectMany(r => XElement.Parse(r).Elements("Result")));
            return el.ToString();
        }

        protected void ResyncAfterUpdate(SPWeb web, SPList list, string result, Dictionary<string, DataRow> rowMap)
        {
            XDocument resultsDoc = XDocument.Parse(result);
            List<DataRow> resyncRows = new List<DataRow>();
            foreach (XElement el in resultsDoc.Root.Elements().Where(e => e.Name == "Result"))
            {
                string methodID = el.Attribute("ID").Value;
                string code = el.Attribute("Code").Value;
                if (!rowMap.ContainsKey(methodID)) continue;

                DataRow row = rowMap[methodID];
                if (code == "0")
                {
                    if (row.RowState == DataRowState.Added)
                    {
                        XElement idEl = el.Elements().Where(e => e.Name == "ID").FirstOrDefault();
                        if (idEl != null)
                        {
                            int id = int.Parse(idEl.Value);
                            row["ID"] = id;
                            resyncRows.Add(row);
                        }
                        row["owshiddenversion"] = 1;
                    }
                    else if (row.RowState == DataRowState.Modified)
                    {
                        resyncRows.Add(row);
                        string hidVers = row["owshiddenversion"].ToString();
                        int vers;
                        if (int.TryParse(hidVers, out vers)) { row["owshiddenversion"] = vers + 1; }
                    }

                    row.ClearErrors();
                    row.AcceptChanges();
                }
                else
                {
                    if (row.RowState == DataRowState.Deleted) row.RejectChanges();
                    row.RowError = el.Value;
                }
            }
            if (!this.Settings.ResyncAfterUpdate || resyncRows.Count == 0) return;

            ApplyResyncChanges(web, list, resyncRows);
        }

        protected void ApplyResyncChanges(SPWeb web, SPList list, List<DataRow> resyncRows)
        {
            // resync in batches - too many CAML OR-s lead to exception
            foreach (DataRow[] partResyncRows in Utility.SplitInChunks<DataRow>(resyncRows.ToArray(), NUMBER_BATCH_ITEMS_RESYNC))
            {
                string idQuery = Utility.GetQueryByIDs(partResyncRows.Select(r => r["ID"].ToString()).ToList());
                GridViewDataSourceSettings oldSettings = this.Settings;
                GridViewDataSourceSettings newSettings = this.Settings.Clone();
                newSettings.Query = idQuery;
                // force recursive all
                newSettings.ViewScope = ViewScope.RecursiveAll;
                // force nil root folder
                newSettings.RootFolder = null;

                DataTable newTable;
                try
                {
                    this.Settings = newSettings;
                    newTable = this.InternalGetDataTable(web, list);
                }
                finally
                {
                    this.Settings = oldSettings;
                }
                Dictionary<string, DataRow> newRowsMap = newTable.Rows.Cast<DataRow>().ToDictionary(r => r["ID"].ToString());
                foreach (DataRow row in partResyncRows)
                {
                    string strID = row["ID"].ToString();
                    if (newRowsMap.ContainsKey(strID))
                    {
                        row.ItemArray = newRowsMap[strID].ItemArray;
                    }
                    row.AcceptChanges();
                }
            }
        }

        protected string GenerateCAML(SPList list, DataTable table, DataRow[] changedRows, Dictionary<string, DataRow> rowMap, bool moderate, ref int nextMethodID)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
            sb.Append("<ows:Batch OnError=\"Continue\">");

            ListContext listContext = new ListContext(list);
            foreach (string method in GenerateCAMLMethods(list, listContext, table, changedRows, rowMap, moderate, ref nextMethodID))
                sb.Append(method);

            sb.Append("</ows:Batch>");
            return sb.ToString();
        }

        protected List<string> GenerateCAMLMethods(SPList list, ListContext listContext, DataTable table, DataRow[] changedRows, Dictionary<string, DataRow> rowMap, bool moderate, ref int nextMethodID)
        {
            string strGuid = list.ID.ToString();

            List<string> methodList = new List<string>();
            List<string> columnNames = table.Columns.Cast<DataColumn>().Where(c => !Utility.UIReadonlyFields.Contains(c.ColumnName) && !SpecialFields.Contains(c.ColumnName)).Select(c => c.ColumnName).ToList();
            bool isLibrary = list is SPDocumentLibrary;
            bool hasFileLeafRef = table.Columns.Contains("FileLeafRef");
            bool hasTitle = table.Columns.Contains("Title");
            string listServerUrl = list.ParentWeb.ServerRelativeUrl.TrimEnd('/') + "/" + list.RootFolder.Url.Trim('/');
            string rootFolder = this.GetEffectiveRootFolder(this.Settings);
            string folderBaseUrl = (string.IsNullOrEmpty(rootFolder) ? listServerUrl : rootFolder).TrimEnd('/') + "/";

            foreach (DataRow row in changedRows)
            {
                StringBuilder sb = new StringBuilder();

                string id = row.HasVersion(DataRowVersion.Original) ? row["ID", DataRowVersion.Original].ToString() : row["ID"].ToString();
                string methodID = "M" + nextMethodID;
                rowMap[methodID] = row;

                string cmdName = row.RowState == DataRowState.Deleted ? "Delete" : (moderate ? "Moderate" : "Save");

                if (string.IsNullOrEmpty(id)) id = "New";
                sb.Append("<Method ID=\"" + methodID + "\"><SetList>" + strGuid + "</SetList>");
                AddSetVar(sb, "Cmd", cmdName);
                AddSetVar(sb, "ID", id);

                if (row.RowState == DataRowState.Added)
                {
                    foreach (string columnName in columnNames)
                    {
                        if (Utility.RequiredFields.Contains(columnName)) continue;
                        string columnValue = this.GetColumnValue(row, columnName, listContext);
                        AddSetVarField(sb, columnName, columnValue);
                    }
                    if (!string.IsNullOrEmpty(rootFolder)) AddSetVar(sb, "RootFolder", rootFolder);
                    string fsObjType = Utility.ParseLookupValue(row["FSObjType"].ToString());
                    if ("1".Equals(fsObjType))
                    {
                        AddSetVar(sb, "Type", "1");
                        string name = string.Empty;
                        if (hasFileLeafRef) name = Utility.ParseLookupValue(row["FileLeafRef"].ToString());
                        if (string.IsNullOrEmpty(name) && hasTitle) name = row["Title"].ToString();
                        AddSetVar(sb, "owsfileref", folderBaseUrl + name);
                    }
                }
                else
                {
                    bool isFolder = false;
                    if (row.RowState == DataRowState.Modified)
                    {
                        string fsObjType = Utility.ParseLookupValue(row["FSObjType", DataRowVersion.Original].ToString());
                        isFolder = "1".Equals(fsObjType);
                    }

                    if (isLibrary || isFolder)
                    {
                        string fileRef = Utility.ParseLookupValue(row["FileRef", DataRowVersion.Original].ToString());
                        if (!fileRef.StartsWith("/")) fileRef = "/" + fileRef;
                        AddSetVar(sb, "owsfileref", fileRef);
                    }

                    if (row.RowState == DataRowState.Modified)
                    {
                        // add the required fields aliases
                        if ((isLibrary || isFolder) && hasFileLeafRef && !row["FileLeafRef", DataRowVersion.Original].Equals(row["FileLeafRef"]))
                        {
                            AddSetVar(sb, "owsnewfileref", Utility.GetFileNameWOExt(Utility.ParseLookupValue(row["FileLeafRef"].ToString())));
                            // to rename a folder in a list we need that
                            if (isFolder) AddSetVar(sb, "Type", "1");
                        }

                        // these are the internally accepted aliases
                        if (!DBNull.Value.Equals(row["_ModerationStatus"]))
                        {
                            AddSetVar(sb, "owshiddenmodstatus", row["_ModerationStatus", DataRowVersion.Original].ToString());
                            AddSetVarField(sb, "_ModerationStatus", row["_ModerationStatus"].ToString());
                        }
                        if (!DBNull.Value.Equals(row["_Level"]))
                        {
                            AddSetVar(sb, "owsitemlevel", row["_Level", DataRowVersion.Original].ToString());
                            AddSetVar(sb, "_Level", row["_Level"].ToString());
                        }

                        AddSetVar(sb, "owshiddenversion", row["owshiddenversion"].ToString());

                        foreach (string columnName in columnNames)
                        {
                            string columnValue = this.GetColumnValue(row, columnName, listContext);
                            if (!columnValue.Equals(this.GetColumnValue(row, columnName, listContext, DataRowVersion.Original)))
                                AddSetVarField(sb, columnName, columnValue);
                        }
                    }
                }
                sb.Append("</Method>");
                methodList.Add(sb.ToString());
                nextMethodID++;
            }

            return methodList;
        }

        protected void AddSetVar(StringBuilder sb, string name, string value)
        {
            sb.AppendFormat("<SetVar Name=\"{0}\">{1}</SetVar>", Utility.EscapeForXml(name), Utility.EscapeForXml(value));
        }

        protected void AddSetVarField(StringBuilder sb, string name, string value)
        {
            sb.AppendFormat("<SetVar Name=\"urn:schemas-microsoft-com:office:office#{0}\">{1}</SetVar>", Utility.EscapeForXml(name), Utility.EscapeForXml(value));
        }

        protected string GetColumnValue(DataRow row, string columnName, ListContext context)
        {
            return GetColumnValue(row, columnName, context, DataRowVersion.Current);
        }

        protected string GetColumnValue(DataRow row, string columnName, ListContext context, DataRowVersion version)
        {
            DateTime date;
            object obj = row[columnName, version];
            string strVal = obj.ToString();
            if (context.GetFieldValueType(columnName) == typeof(DateTime))
            {
                if (obj is DateTime)
                {
                    date = (DateTime)obj;
                    goto formatdate;
                }
                else if (obj is string)
                {
                    if (DateTime.TryParse(strVal, CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
                    {
                        goto formatdate;
                    }
                }
            }
            else if (obj is double) strVal = ((double)obj).ToString(CultureInfo.InvariantCulture);
            else if (obj is int) strVal = ((int)obj).ToString(CultureInfo.InvariantCulture);

            return strVal;
        formatdate:
            return date.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
        }

        protected List<string> FixViewFieldSchema(SPList list, List<string> viewFields)
        {
            if (viewFields == null) viewFields = new List<string>();
            
            List<string> newViewFields = new List<string>();
            HashSet<string> fieldNames = new HashSet<string>();
            for (int i = 0; i < viewFields.Count; i++ )
            {
                string name = viewFields[i];
            recheck:
                if (fieldNames.Contains(name)) continue;
                SPField field = null;
                try
                {
                    field = list.Fields.GetFieldByInternalName(name);
                }
                catch { }

                if (field == null) continue;
                if (field is SPFieldComputed 
#if SHAREPOINT2010
                    && field.InternalName != "ContentType"
#endif
                    )
                {
                    // try to get the first reference field of the computed field
                    SPFieldComputed compField = field as SPFieldComputed;
                    if (compField.FieldReferences != null && compField.FieldReferences.Length > 0)
                    {
                        name = compField.FieldReferences[0];
                        goto recheck;
                    }
                    continue;
                }
                else if (IsFieldTypeNotAllowed(field)) continue;

                newViewFields.Add(name);
                fieldNames.Add(name);
            }
            return newViewFields;
        }

        protected List<string> GetEffectiveViewFields (bool isLibrary)
        {
            return Utility.GetViewFieldWithRequired(this.Settings.ViewFields, isLibrary);
        }

        protected List<string> GetViewFieldsFromView(SPView view)
        {
            // just to be sure we have the internal names
            return XDocument.Parse("<r>" + view.ViewFields.SchemaXml + "</r>").Root.Elements().Where(el => el.Name == "FieldRef").Select(el => el.Attribute("Name").Value).ToList();
        }

        protected void OpenUseList(UseList useList)
        {
            Utility.OpenUseList(this.Settings, useList, true);
        }

        protected string FormatViewFields(List<string> viewFields)
        {
            return string.Concat(viewFields.Select(s => string.Format("<FieldRef Name='{0}' Nullable='TRUE' />", s)).ToArray());
        }

        protected static void EnableEventFiring()
        {
            EventFiringDisabled.SetValue(null, false, null);
        }

        protected static void DisableEventFiring()
        {
            EventFiringDisabled.SetValue(null, true, null);
        }

        #endregion
    }
}